// Project: Salient
// http://salient.codeplex.com
// 
// Copyright 2010, Sky Sanders <sky at skysanders.net>
// Dual licensed under the MIT or GPL Version 2 licenses.
// http://salient.codeplex.com/license
// 
// Date: April 16 2010 

#region

using System;
using System.ServiceModel;
using System.Text.RegularExpressions;
using System.Web.Script.Serialization;

#endregion

namespace Salient.Web
{
    ///<summary>
    ///</summary>
    public sealed class WebExceptionDetail : ExceptionDetail
    {
        ///<summary>
        ///</summary>
        public WebExceptionDetail()
            : base(new Exception("ignore"))
        {
        }

        private WebExceptionDetail(Exception exception)
            : base(exception)
        {
        }

        ///<summary>
        ///</summary>
        public WebErrorType ErrorType { get; set; }


        ///<summary>
        ///</summary>
        public new string Type { get; set; }


        ///<summary>
        ///</summary>
        public new WebExceptionDetail InnerException { get; set; }

        ///<summary>
        ///</summary>
        public new string Message { get; set; }

        ///<summary>
        ///</summary>
        public new string StackTrace { get; set; }

        #region Factory Methods

        ///<summary>
        ///</summary>
        ///<param name="text"></param>
        ///<returns></returns>
        public static WebExceptionDetail Create(string text)
        {
            WebExceptionDetail returnValue;

            if (TryParseAspNet(text, out returnValue))
            {
                return returnValue;
            }

            if (TryParseJsonApp(text, out returnValue))
            {
                return returnValue;
            }

            if (TryParseJsonText(text, out returnValue))
            {
                return returnValue;
            }

            return new WebExceptionDetail { ErrorType = WebErrorType.UNKNOWN, StackTrace = text };
        }

        /// <summary>
        /// 
        /// A ScriptService called with text/json will return exception responses like these
        /// 
        /// 
        /// System.NotImplementedException: Intentional
        ///    at TestSite.Services.ScriptService.ThrowException() in C:\Projects\salient\Releases\TestingWithVSDevServer_v1\TestSite\Services\ScriptService.asmx.cs:line 27
        /// 
        /// 
        /// System.NotImplementedException: Intentional ---> System.ArgumentNullException: inner
        /// Parameter name: arg
        ///    at TestSite.Services.ScriptService.ThrowInner() in C:\Projects\salient\Releases\TestingWithVSDevServer_v1\TestSite\Services\ScriptService.asmx.cs:line 45
        ///    at TestSite.Services.ScriptService.ThrowException() in C:\Projects\salient\Releases\TestingWithVSDevServer_v1\TestSite\Services\ScriptService.asmx.cs:line 29
        ///    --- End of inner exception stack trace ---
        ///    at TestSite.Services.ScriptService.ThrowException() in C:\Projects\salient\Releases\TestingWithVSDevServer_v1\TestSite\Services\ScriptService.asmx.cs:line 33
        /// 
        /// 
        /// System.NotImplementedException: Intentional ---> System.Exception: inner
        ///    --- End of inner exception stack trace ---
        ///    at TestSite.Services.ScriptService.ThrowException() in C:\Projects\salient\Releases\TestingWithVSDevServer_v1\TestSite\Services\ScriptService.asmx.cs:line 28
        /// 
        /// </summary>
        /// <param name="text"></param>
        /// <param name="exceptionDetail"></param>
        /// <returns></returns>
        private static bool TryParseJsonText(string text, out WebExceptionDetail exceptionDetail)
        {
            Match match = Regex.Match(text, @"^(?<type>[\w\.]+):(?<message>.*?)(?:(\n\s{3}at\s)|(\s--->\s))",
                                      RegexOptions.ExplicitCapture | RegexOptions.Singleline |
                                      RegexOptions.ExplicitCapture);
            if (match.Success)
            {
                exceptionDetail = new WebExceptionDetail
                    {
                        ErrorType = WebErrorType.PLAINTEXT,
                        Type = match.Groups["type"].Value.Trim(),
                        Message = match.Groups["message"].Value.Trim(),
                        StackTrace = text
                    };
                return true;
            }

            exceptionDetail = null;
            return false;
        }

        /// <summary>
        /// Most ScriptService endpoints when called with application/json
        /// 
        ///  {
        ///      "ExceptionDetail": {
        ///          "HelpLink": null,
        ///          "InnerException": null,
        ///          "Message": "You fooed the bar",
        ///          "StackTrace": "(... lots of text ...)",
        ///          "Type": "System.InvalidOperationException"
        ///      },
        ///      "ExceptionType": "System.InvalidOperationException",
        ///      "Message": "You fooed the bar",
        ///      "StackTrace": "(... lots of text ...)",
        ///  }
        /// </summary>
        /// <param name="text"></param>
        /// <param name="exceptionDetail"></param>
        /// <returns></returns>
        private static bool TryParseJsonApp(string text, out WebExceptionDetail exceptionDetail)
        {
            if (text.StartsWith("{") &&
                text.Contains("\"Message\"") &&
                text.Contains("\"StackTrace\"") &&
                text.Contains("\"ExceptionType\""))
            {
                // If we change ExceptionType of the outer exception it will be shaped
                // like a System.ServiceModel.ExceptionDetail, which we can deserialize to
                text = text.Replace("\"ExceptionType\"", "\"Type\"");
                exceptionDetail = new JavaScriptSerializer().Deserialize<WebExceptionDetail>(text);
                exceptionDetail.ErrorType = WebErrorType.JSON;
                return true;
            }
            exceptionDetail = null;
            return false;
        }

        private static bool TryParseAspNet(string text, out WebExceptionDetail exceptionDetail)
        {
            if (Regex.IsMatch(text, @"<!--\s*\[[\w\.`]+\].*-->$", RegexOptions.Singleline))
            {
                Regex rx = new Regex(@"\[(?<type>[\w\.`]+)\]:\s(?<message>(.(?!\n\s{3}at\s))*)", RegexOptions.Singleline);
                exceptionDetail = Parse(text, WebErrorType.ASPNET, rx);
                return true;
            }

            exceptionDetail = null;
            return false;
        }

        private static WebExceptionDetail Parse(string text, WebErrorType errorType, Regex rx)
        {
            WebExceptionDetail current = null;
            WebExceptionDetail toReturn = null;
            MatchCollection matches = rx.Matches(text);
            for (int i = matches.Count - 1; i > -1; i--)
            {
                Match match = matches[i];

                string exType = match.Groups["type"].Value.Trim();

                if (exType.IndexOf("HttpUnhandledException") > -1 || exType.IndexOf("TargetInvocationExceptionWrapper") > -1)
                {
                    // we don't care to see this exception
                    continue;
                }
                WebExceptionDetail ext = new WebExceptionDetail
                    {
                        ErrorType = errorType,
                        Type = exType,
                        Message = match.Groups["message"].Value.Trim()
                    };
                if (current != null)
                {
                    current.InnerException = ext;
                }
                else
                {
                    toReturn = ext;
                }
                current = ext;
            }
            if (toReturn != null)
            {
                var rxStack = new Regex("<!--(?<stack>.*?)-->$", RegexOptions.Singleline | RegexOptions.ExplicitCapture);
                if (rxStack.IsMatch(text))
                {
                    toReturn.StackTrace = rxStack.Match(text).Groups["stack"].Value;
                }
                else
                {
                    toReturn.StackTrace = text;    
                }
            }
            return toReturn;
        }


        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return string.Format("{0}: {1}", Type ?? Type, Message);
        }

        #endregion


        // TODO:revise this
        // When a JSON enpoint throws any exception it presents to our HttpWebRequest as a generic WebException (500 Server Error). This does not present much information to work with in order to appropriately handle the exception.
        // 
        // To discover the actual exception that was thrown we need to catch the WebException and extract the content that was served from the WebException.Response.GetResponseStream().
        // 
        // This is always going to be text, but it can be of serveral different formats. It can be a simple status message, an ASP.Net Yellow Screen of Death, a blue IIS generic error page or, in the case of JSON endpoints, a JSON string representing the actual exception.
        // 
        // Each of these types of web errors leaves us to do much work in order to properly identify the exception. 
        // 
        // RequestBuilder.Request.Create(), which is the base method call by all of our helpers, handles all of this by trapping any WebException throw when the request is executed and re-throwing a more detailed exception.
        // 
        // While we will cover calling other types of endpoints and handling the possible exceptions later in this walkthrough, the primary focus at this point is JSON endpoints.
        // 
        // In this type of scenario, the JSON endpoint that thows the exception thinks it is being called by JavaScript so the exception information it provides is an 'untyped' JSON string like so:
        // 
        //  {
        //      "ExceptionDetail": {
        //          "HelpLink": null,
        //          "InnerException": null,
        //          "Message": "You fooed the bar",
        //          "StackTrace": "(... lots of text ...)",
        //          "Type": "System.InvalidOperationException"
        //      },
        //      "ExceptionType": "System.InvalidOperationException",
        //      "Message": "You fooed the bar",
        //      "StackTrace": "(... lots of text ...)",
        //  }
        // 
        // 
        // This JSON represents a serialized System.ServiceModel.Description.WebScriptEnablingBehavior+JsonFaultDetail. This is an internal nested type making it quite impractical to use the original type to deserialize the JSON.
        // 
        // This problem is easily solved by defining a public class of the same shape. As an added bonus we will derive our JsonFaultDetail from System.Exception so that it may be re-thrown.
        // 
        //  The next task is to determine if the JSON response from one of our helper methods is a JsonFault. We may come up with a more elegant way to do this later but for now we will simply scanning the string for expected properties in a static method on our public JsonFaultDetail class.
        // 
        //  With the requirements of a public destination type and the ability to identify a JsonDetailFault we can now add a static helper method, .Check(JSON), to JsonFaultDetail that will check the response stream for a JsonFaultDetail. If one is found, it is deserialized and thrown.

    }
}